#!/usr/bin/env python

"""
Client
Copyright (C) 2007 Andrew Briscoe

The a telnet based client for the PIGS server.
"""

__license__ = 'GPL http://www.gnu.org/licenses/gpl.txt'
__author__ = 'Andrew Briscoe <everynothing@gmail.com>'
__version__ = '0.0.1'
__program__ = 'PIGS'

import RandomStuff, wx, threading, Server, Display, Game, sys
from twisted.internet import reactor
from twisted.python import threadable
import wx.lib.mixins.listctrl as mix
threadable.init(1)

class DaemonLoop(threading.Thread):
    "The twisted main loop."
    
    def __init__(self, wxEvtHandler):
        
        super(DaemonLoop, self).__init__()
        self.ui = RandomStuff.UIProxy(wxEvtHandler)
        self.handle, self.last_send =  None, None
        self.factory = RandomStuff.PIGSFactory(self)
        self.factory.protocol = RandomStuff.PIGSClient

    def run(self):
        "Twisteds main loop"
        
        RandomStuff.run_and_install(reactor, self.factory)
        
    def send_array(self, array):
        "Sends some text."
        
        self.last_send = array
        self.factory.send_message(array)
        
    def process_packet(self, text):
        "The server has sent some text, process it!"
        
        tok = text.split(Server.SEPARATE)
        typ, gui = tok[0], self.ui
        double_split = [t.split(Server.MINI_SEPARATE) for t in tok[1:]]
        print typ + '\t' + str(double_split)
        
        if typ == 'act':
            gui.execute_action(int(tok[1]), tok[2])
        elif typ == 'chat':
            gui.append_text(tok[1], tok[2])
        elif typ == 'request':
            gui.request_dialog(tok[1])
        elif typ == 'joined room':
            gui.create_chat(tok[1])
        elif typ == 'seeks':
            gui.update_seeks(double_split)
        elif typ == 'channels':
            gui.update_chats(double_split)
        elif typ == 'games':
            gui.update_games(double_split)
        elif typ == 'players':
            gui.update_players(tok[1], double_split[1:])
        elif typ == 'started game':
            gui.create_board(RandomStuff.game_from_strings(tok[2], tok[3]), 
                             int(tok[1]), int(tok[5]), tok[6:])
        elif typ == Server.REQUEST_LOGIN:
            gui.show_login_dialog()
        elif typ == 'good login':
            self.handle = tok[1]
        else:
            gui.popup(typ)
        
    def get_handle(self):
        "So the ui can access handle."
        
        return self.handle
    
class UIProxyReceiver(object):
    def __init__(self):
        self._proxy_enabled = True

    def DisableProxy(self):
        self._proxy_enabled = False

class PIGSClientFrame(wx.Frame, UIProxyReceiver):
    "The main fram for the client."
    
    def __init__(self):
        
        wx.Frame.__init__(self, None, -1, "PIGS Client", size=(800, 600))
        UIProxyReceiver.__init__(self)
        self.make_menu()
        self.games, self.chats = [], []
        self.tabs = wx.Notebook(self, -1, style=wx.NB_TOP)
        self.game_list, self.seek_list, self.chat_list = \
            GameList(self), SeekList(self), ChatList(self)
        self.tabs.AddPage(self.game_list, "Games")
        self.tabs.AddPage(self.seek_list, "Seeks")
        self.tabs.AddPage(self.chat_list, "Chats")
        RandomStuff.fit_center_show(self, False)
        self.Bind(wx.EVT_CLOSE, self.on_close)
        _daemon = DaemonLoop(self)
        _daemon.setDaemon(True)
        _daemon.start()
        self.daemon = RandomStuff.TwistedProxy(_daemon)
        
    def create_board(self, game, number, player, players):
        "Make a new board window."
        
        self.games += [Display.make_game_frame(self, game, number, 
                                               player, players)]
        
    def create_chat(self, name):
        "Make a new board window."
        
        con = Console(self.tabs, self, name)
        self.tabs.AddPage(con, name + " Chat")
        con.SetFocus()
    
    def execute_action(self, game_number, action):
        "Does action on the game with number."
        
        gam = self.game_by_number(game_number)
        gam.execute_action(Game.action_from_string(action, gam.game.board))
        
    def popup(self, text):
        "Makes a popup with text."
        
        dlg = wx.MessageDialog(self, text, style=wx.OK|wx.CENTRE)
        dlg.ShowModal()
        dlg.Destroy()
        
    def show_login_dialog(self):
        "Shows this login dialog."
        
        if not self.daemon.get_handle():
            LoginDialog(self)
            
    def game_by_number(self, number):
        "Return a game by its number."
        
        for game in self.games:
            if game.number == number:
                return game
            
    def room_from_name(self, room):
        "Grabs the room with name."
        
        for chat in self.chats:
            if chat.name == room:
                return chat
        
    def append_text(self, room, text):
        "Show some text"
        
        self.room_from_name(room).append_text(text + '\n')
        
    def send_array(self, array):
        
        self.daemon.send_array(array)
        
    def chat(self, channel, text):
        "Write some text to the transport."
        
        self.send_array(['chat', channel, text])

    def do_exit(self, event=None):
        "User quit."
        
        for game in self.games:
            game.Close(True)
        self.Close(True)
        
    def update_seeks(self, array):
        "Update the seek display."
        
        self.seek_list.update(array)
        
    def update_games(self, array):
        "Update the seek display."
        
        self.game_list.update(array)
        
    def update_players(self, room, array):
        "Player list."
        
        self.room_from_name(room).player_list.update(array)
        
    def update_chats(self, array):
        "Update the seek display."
        
        self.chat_list.update(array)
        
    def seek_frame(self, event=None):
        "Make a new seek frame."
        
        PickGameFrame(self)
        
    def create_channel_frame(self, event=None):
        "Make a channel creation frame."
        
        NewChatDialog(self)
        
    def on_close(self, event=None):
        "What happens when the client is closed?"
        
        for game in self.games:
            game.on_close()
        self.send_array(['disconnecting'])
        self.Destroy()
        
    def request_dialog(self, request):
        "Make a request dialog."
        
        dlg = wx.MessageDialog(self, 'Your opponent' + 'has requested a ' \
               + request.split(' ')[0] + '.  Accept?', 'Request', 
               wx.YES_NO | wx.NO_DEFAULT | wx.ICON_QUESTION)
        if dlg.ShowModal() == wx.ID_YES:
            self.send_array([request])
        dlg.Destroy()
        
    def make_menu(self):
        "Sets up the menu."
        
        menu = ("&File", 
        [(self.seek_frame, "S&eek", "Look for a game."), 
         (self.create_channel_frame, "N&ew Channel", "Create new chat channel."), 
         (self.do_exit, "E&xit", "Terminate the program")])
        setup_menu(self, [menu])
        
class Console(wx.Panel):
    "A chatting console"
    
    def __init__(self, parent, control, name):
        
        wx.Panel.__init__(self, parent)
        self.name, self.control = name, control
        self.control.chats += [self]
        self.txt = wx.TextCtrl(self, style=wx.TE_MULTILINE)
        self.txt.SetEditable(False)
        self.enter = wx.TextCtrl(self, style=wx.TE_PROCESS_ENTER)
        self.Bind(wx.EVT_TEXT_ENTER, self.text_entered)
        chat_panel = wx.BoxSizer(wx.VERTICAL)
        chat_panel.Add(self.txt, 1, wx.EXPAND | wx.ALL)
        chat_panel.Add(self.enter, flag=wx.GROW)
        main_panel = wx.BoxSizer(wx.HORIZONTAL)
        main_panel.Add(chat_panel, 3, wx.EXPAND)
        self.player_list = PlayerList(self.control, self)
        main_panel.Add(self.player_list, 1, wx.EXPAND)
        sizer = wx.BoxSizer(wx.VERTICAL)
        if self.name.split(' ')[0] != 'Game!->':
            but = make_std_buttons(self, self.control, True)
            sizer.Add(but)
        sizer.Add(main_panel, 1, wx.EXPAND|wx.ALL)
        self.SetSizer(sizer)
    
    def text_entered(self, event=None):
        'User pressed enter.'
        
        txt = self.enter.GetValue()
        if txt:
            self.control.chat(self.name, txt)
            self.enter.Clear()
            
    def append_text(self, text):
        "Appends txt to self.txt."
        
        self.txt.AppendText(text)
        
    def basic_close(self, event=None):
        "For when a game closes it."
        
        self.control.send_array(['leave chat', self.name])
        if self in self.control.chats:
            self.control.chats.remove(self)
        
    def close(self, event=None):
        "Closes this panel."
        
        self.basic_close()
        self.control.tabs.DeletePage(self.control.tabs.GetSelection())
        
def setup_menu(window, menus):
    "Sets up the menus for window."
    
    menu_bar = wx.MenuBar()
    for title, acts in menus:
        menu = wx.Menu()
        for act, label, hlp in acts:
            item = menu.Append(-1, text=label, help=hlp)
            window.Bind(wx.EVT_MENU, act, item)
        menu_bar.Append(menu, title)
    window.SetMenuBar(menu_bar)
    window.statusbar = window.CreateStatusBar()
        
def make_std_buttons(parent, control, show_close=False):
    "Make the standard set of buttons."
    
    buts = [("Seek", control.seek_frame), 
            ("New Chat", control.create_channel_frame)]
    if show_close:
        buts += [('Close', parent.close)]
    return make_buttons(parent, buts)
        
class ListPanel(wx.Panel):
    "A panel for showing availible games."
    
    CLICK_MESSAGE, COLUMNS, SHOW_BUTTONS = '', [], True
    
    def __init__(self, control, parent=None):
        
        if not parent:
            parent = control.tabs
        wx.Panel.__init__(self, parent, style = wx.DEFAULT_FRAME_STYLE)
        self.control = control
        self.list = AdvancedListCtrl(self, len(self.COLUMNS))
        for cnt in xrange(len(self.COLUMNS)):
            self.list.InsertColumn(cnt, self.COLUMNS[cnt])
        self.Bind(wx.EVT_LIST_ITEM_SELECTED, self.selected_event)
        sizer = wx.BoxSizer(wx.VERTICAL)
        if self.SHOW_BUTTONS:
            buts = make_std_buttons(self, self.control)
            sizer.Add(buts)
        sizer.Add(self.list, 1, wx.EXPAND)
        self.SetSizer(sizer)
        
    def update(self, new_values):
        "List has been refreshed, update it with new_values."
        
        self.list.DeleteAllItems()
        self.list.itemDataMap = dict([(x, new_values[x]) \
                                      for x in xrange(len(new_values))])
        cnt = 0
        for items in new_values:
            index = self.list.InsertStringItem(sys.maxint, items[0])
            cnt2 = 0
            for item in items:
                self.list.SetStringItem(index, cnt2, item)
                cnt2 += 1
            self.list.SetItemData(index, cnt)
            cnt += 1
                
    def selected_event(self, event=None):
        "What happens when a user clicks on an item."
        
        self.control.send_array([self.CLICK_MESSAGE, 
             self.list.GetItemText(self.list.GetFocusedItem())])
        
class AdvancedListCtrl(wx.ListCtrl, mix.ListCtrlAutoWidthMixin, mix.ColumnSorterMixin):
    
    def __init__(self, parent, leng):
        
        wx.ListCtrl.__init__(self, parent, style=wx.LC_REPORT)
        mix.ListCtrlAutoWidthMixin.__init__(self)
        mix.ColumnSorterMixin.__init__(self, leng)
        
    def GetListCtrl(self):
        "Sp it can be sorted."
        
        return self

class GameList(ListPanel):
    "A panel for showing availible games."
    
    CLICK_MESSAGE = 'join game'
    COLUMNS = ['Number', 'Player 1', 'Player 2', 'Game', 'Board', 'Variant']    
class SeekList(ListPanel):
    "A panel for showing availible seeks."
    
    CLICK_MESSAGE = 'accept seek'
    COLUMNS = ['Number', 'Seeker', 'Game', 'Board', 'Variant', 'Description']
    
class ChatList(ListPanel):
    "A panel for showing availible chat rooms."
    
    CLICK_MESSAGE, COLUMNS = 'join channel', ['Name', 'Description']
    
class PlayerList(ListPanel):
    "A panel for showing all players in a chat room."
    
    CLICK_MESSAGE, COLUMNS, SHOW_BUTTONS = 'get info', ['Chatter'], False
            
class PickGameFrame(wx.Frame):
    'When the user is selecting what type of new game to play.'
    
    def __init__(self, parent):
        
        def how_many_empty(list):
            'How many spots do the lists need?'
            
            return max([len(x) for x in list])
        
        wx.Frame.__init__(self, None, -1, 'New Game')
        self.parent = parent
        txts = [wx.StaticText(self, label=name, style=wx.ALIGN_CENTRE) \
                for name in ["Game Type:", "Board Type:", 
                             "Variant:", "Description:"]]
        vals = [g for g in RandomStuff.possible_games().values()]
        self.len_boards = how_many_empty([g.POSSIBLE_BOARDS for g in vals])
        self.len_variants = how_many_empty([g.POSSIBLE_VARIANTS for g in vals])
        choices = [RandomStuff.possible_games().keys(), [' '] * \
                   self.len_boards, [' '] * self.len_variants]
        self.game, self.board, self.variant = [ wx.ListBox(self, 
           choices=choices[x], style=wx.CB_READONLY) for x in xrange(3)]
        self.game.Bind(wx.EVT_LISTBOX, self.new_game_picked)
        self.game.SetSelection(0)
        self.new_game_picked()
        self.description = wx.TextCtrl(self, 
                           style=wx.TE_PROCESS_ENTER, size=(200, 20))
        self.Bind(wx.EVT_TEXT_ENTER, self.on_start)
        buttons = make_buttons(self, [('Start', self.on_start), 
                                      ('Close', self.on_close)])
        sizer = wx.BoxSizer(wx.VERTICAL)
        for item in [txts[0], self.game, txts[1], self.board, 
                     txts[2], self.variant, txts[3], self.description, buttons]:
            sizer.Add(item, flag=wx.GROW)
        self.SetSizer(sizer)
        RandomStuff.fit_center_show(self)
    
    def new_game_picked(self, event=None):
        'A new game was picked, refresh variants and maps.'
        
        game = RandomStuff.possible_games()[self.get_selected(self.game)]
        for ref in [(game.POSSIBLE_BOARDS, self.len_boards, self.board), 
                    (game.POSSIBLE_VARIANTS, self.len_variants, self.variant)]:
            for cnt in xrange(ref[1]):
                name = ref[0][cnt] if cnt < len(ref[0]) else ' '
                ref[2].SetString(cnt, name)
            ref[2].SetSelection(0)
        
    def get_selected(self, lst):
        'What game is selected?'
        
        return lst.GetString(lst.GetSelection())
        
    def on_start(self, event=None):
        'The user clicked start, so bring up the second menu.'
        
        self.parent.send_array(['seek', self.get_selected(self.game),
            self.get_selected(self.board), self.get_selected(self.variant), 
            self.description.GetValue()])
        self.Close()
        
    def on_close(self, event=None):
        'Closes this window.'
        
        self.Close()
        
class LoginDialog(wx.Frame):
    "The dialog when you log in."
    
    def __init__(self, parent):
        
        wx.Frame.__init__(self, None, title="Login")
        self.parent = parent
        sizer = wx.BoxSizer(wx.VERTICAL)
        self.sent = False
        self.enter = wx.TextCtrl(self, style=wx.TE_PROCESS_ENTER)
        self.Bind(wx.EVT_TEXT_ENTER, self.login)
        buttons = make_buttons(self, [('Login', self.login), 
                                      ('Cancel', self.on_close)])
        sizer = wx.BoxSizer(wx.VERTICAL)
        sizer.Add(self.enter, 1, wx.EXPAND | wx.ALL)
        sizer.Add(buttons, 1, wx.EXPAND | wx.ALL)
        self.SetSizer(sizer)
        RandomStuff.fit_center_show(self)
    
    def login(self, event=None):
        'User pressed enter.'
        
        self.parent.send_array([self.enter.GetValue()])
        self.sent = True
        self.on_close()
        
    def on_close(self, event=None):
        'Closes this window.'
        
        if self.sent:
            self.Close()
        
class NewChatDialog(wx.Frame):
    "The dialog when you log in."
    
    def __init__(self, parent):
        
        wx.Frame.__init__(self, None, title="New Chat room.")
        self.parent = parent
        sizer = wx.BoxSizer(wx.VERTICAL)
        name_txt, desc_txt = [wx.StaticText(self, label=x) \
                               for x in ["Channel Name:", "Description:"]]
        self.name, self.description = [wx.TextCtrl(self, 
               style=wx.TE_PROCESS_ENTER, size=(200, 20)) for x in xrange(2)]
        self.Bind(wx.EVT_TEXT_ENTER, self.create)
        buttons = make_buttons(self, [('Create', self.create), 
                                      ('Cancel', self.on_close)])
        sizer = wx.BoxSizer(wx.VERTICAL)
        for item in [name_txt, self.name, desc_txt, self.description, buttons]:
            sizer.Add(item, wx.EXPAND|wx.ALL)
        self.SetSizer(sizer)
        RandomStuff.fit_center_show(self)
    
    def create(self, event=None):
        'User pressed enter.'
        
        name, description = self.name.GetValue(), self.description.GetValue()
        if name:
            self.parent.send_array(['create', name, description])
            self.on_close()
        
    def on_close(self, event=None):
        'Closes this window.'
        
        self.Close()
        
def make_buttons(parent, array, sizer=None):
    "Returns a sizer with buttons from array."
    
    if not sizer:
        sizer = wx.BoxSizer(wx.HORIZONTAL)
    for but in array:
        button = wx.Button(parent, label=but[0])
        sizer.Add(button, 1, wx.EXPAND | wx.ALL)
        button.Bind(wx.EVT_BUTTON, but[1])
    return sizer

if __name__ == '__main__':
    appp = RandomStuff.MyApp(0)
    appp.MainLoop()